Skip to content

Check Frame function calls at publish validation - #32282

Open
davidebbo wants to merge 1 commit into
mainfrom
frame-function-validator
Open

Check Frame function calls at publish validation#32282
davidebbo wants to merge 1 commit into
mainfrom
frame-function-validator

Conversation

@davidebbo

@davidebbo davidebbo commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Fixes https://github.com/dust-tt/tasks/issues/10366

The Frames v2 equivalent of the publish-time check that validate_frame_pod_functions.ts used to do for Pod functions, rebuilt against the thing Frames actually call: their own manifest functions.

dsbx frame validate now reports UI code that calls a function the manifest does not declare, or that passes an input the function's schema.input rejects. Both otherwise surface only once a viewer triggers the call — and nothing else type-checks a v2 Frame's UI source, since esbuild strips types without checking them. This is the first thing that reads that source with a compiler.

Two passes, deliberately different

References are checked syntactically, not through the type system, so the message can say what is actually wrong. resolveFrameFunctionReference accepts exactly one form for v2 — a bare manifest name — so the valid set is known exactly, and a bad reference gets either the list of declared functions or, for a leftover <podId>/<slug> Pod reference, the bare name to use instead. Computed references are skipped rather than guessed at. This pass also runs when the manifest declares no functions at all, which the old validator could not do: an empty contract map makes keyof resolve to never and rejects every call with a misleading "not found".

Inputs are checked by a virtual TypeScript program over the Frame's sources, with a generated @dust/react-hooks declaration mapping each declared function name to the input type compiled from its published JSON Schema. Only diagnostics anchored inside a call's input argument are kept, which is what keeps the Frame's own unchecked type errors out of the report — there is a test for that.

The declaration types the input through a conditional on the inferred name rather than constraining the name to keyof, and that detail is load-bearing in both directions. A name outside the map resolves the input to any, so an unknown or computed reference falls through untyped instead of cascading onto the input that the reference pass already reported. And unlike the overload pair I first wrote, there is no permissive signature for a mistyped input to fall back to — TypeScript picks the first applicable overload, so a wrong input simply made the strict overload inapplicable and the call passed silently. That version reported nothing and looked like it worked.

Both hooks are covered, including usePodFunctionMutation, whose input arrives at the trigger it returns rather than at the hook call: the generic flows into the returned type, so trigger({ title: 42 }) is checked at the trigger call site. Aliased and namespace imports are followed.

Advisory by construction

Contracts are authored in Zod and checked here through their extracted JSON Schema, so runtime-only refinements are not always expressible as TypeScript types — an input can pass here and still fail the authoritative Zod validation, and heuristics run in the other direction too. Everything is a ValidationWarning, the input pass is wrapped so a schema the converter cannot express or a compiler failure cannot block a publish, and there is a code contract (frame-function-check-stays-advisory) saying so, because the tempting next step is to promote a finding to a FramePublicationError.

ValidationWarning.type gains a frame_function member. Nothing switches on the field — it is only rendered — and frame_function: … reads better than typescript: … for a missing-function reference.

Not in scope

Output shapes. outputSchema is extracted at publish time and typing data would catch a UI reading a field its function never returns, but those diagnostics land outside the call arguments, where nothing separates them from the Frame's own uncompiled type errors. Worth doing behind a narrower filter; not worth the false-positive budget here.

Testing

15 unit tests on the pass itself (pure, no DB): each warning kind, both hooks, aliased and namespace imports, computed references, a call in an imported component, the empty-manifest case, the cap, and a Frame whose unrelated type error must not be reported. front: 31 files / 226 tests pass across frames, viz, skills and interactive-content. front-api frames routes: 6 files / 25 tests. Typecheck clean in front and front-api; biome clean.

One finding worth noting from writing the tests: a misspelled property produces TS2561 ("Did you mean to write 'title'?"), not the 2353/2345 the old validator listed, so the typo case — the most likely mistake of all — would have gone unreported on the inherited code list.

`dsbx frame validate` now reports UI code that calls a function the Frame's
manifest does not declare, or that passes an input the function's
`schema.input` rejects. Both otherwise fail only once a viewer triggers the
call, and nothing else type-checks a v2 Frame's UI source: esbuild strips types
without checking them.

Two passes. The reference pass is syntactic, so it can name what went wrong: a
literal that matches no declared function, or a leftover `<podId>/<slug>` Pod
reference, which `resolveFrameFunctionReference` rejects outright for v2.
Computed references are skipped. The input pass builds a virtual TypeScript
program over the Frame's sources with a generated `@dust/react-hooks`
declaration whose hooks map a function name to the input type compiled from its
published JSON Schema, and keeps only the diagnostics anchored inside a call's
input argument.

Warnings, never errors, and the input pass is wrapped: a schema the converter
cannot express must not stand between an author and a publish.
@davidebbo
davidebbo force-pushed the frame-function-validator branch from d49864f to 789f402 Compare September 11, 2026 06:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant